﻿using Cognitum.CassandraTableStorage;
using Cognitum.CassandraTableStorage.Entities;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Provider;
using System.Data.Services.Client;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web.Security;

namespace Cognitum.CassandraProviders.Cassandra
{
    public class CassandraRoleProvider : RoleProvider
    {
        #region | Private Methods |

        private string _adminName;
        private string _connectionString;
        private int _userIsOnlineTimeWindow;
        private int _replicationFactor;

        #endregion | Private Methods |

        #region | Public Properties |

        public override string ApplicationName { get; set; }

        #endregion | Public Properties |

        #region | Public Methods |

        public override void AddUsersToRoles(string[] usernames, string[] rolenames)
        {
            if (rolenames.Any(rolename => !RoleExists(rolename)))
                throw new ProviderException("Role name not found.");
            for (int i = 0; i < usernames.Count(); i++)
            {
                usernames[i] = usernames[i].ToLower();
            }
            if (usernames.Any(username => rolenames.Any(rolename => IsUserInRole(username, rolename))))
                throw new ProviderException("User is already in role.");

            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            foreach (string username in usernames)
                foreach (string rolename in rolenames)
                    roleDataSource.Insert(
                        new role_user_entity()
                        {
                            application_name = this.ApplicationName,
                            role_name = rolename,
                            user_name = username
                        });
        }

        public override void CreateRole(string rolename)
        {
            if (RoleExists(rolename))
                throw new ProviderException("Role name already exists.");

            // Add the admin as the first user to any new role that is created
            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            try
            {
                roleDataSource.Insert(new
                    role_entity()
                {
                    application_name = this.ApplicationName,
                    role_name = rolename
                });
            }
            catch (DataServiceRequestException)
            {
                // Nothing to do
            }
        }

        public override bool DeleteRole(string rolename, bool throwOnPopulatedRole)
        {
            if (!RoleExists(rolename))
                throw new ProviderException("Role does not exist.");

            if (throwOnPopulatedRole && GetUsersInRole(rolename).Length > 0)
                throw new ProviderException("Cannot delete a populated role.");

            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            roleDataSource.DeleteRole(rolename, ApplicationName);
            return true;
        }

        public override string[] FindUsersInRole(string rolename, string usernameToMatch)
        {
            usernameToMatch = usernameToMatch.ToLower();
            ArrayList roleNames = new ArrayList();

            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<role_user_entity> roles = roleDataSource.FindUsersInRole(rolename, ApplicationName, usernameToMatch).OrderBy(x => x.user_name);

            foreach (role_user_entity roleEntity in roles)
                roleNames.Add(roleEntity.user_name);

            return (string[])roleNames.ToArray(typeof(string));
        }

        public override string[] GetAllRoles()
        {
            ArrayList roleNames = new ArrayList();

            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            IEnumerable<role_entity> roles = roleDataSource.GetAllRoles(ApplicationName);

            foreach (role_entity roleEntity in roles)
                roleNames.Add(roleEntity.role_name);

            return (string[])roleNames.ToArray(typeof(string));
        }

        public override string[] GetRolesForUser(string username)
        {
            username = username.ToLower();
            ArrayList roleNames = new ArrayList();

            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);
            IEnumerable<role_user_entity> roles = roleDataSource.GetRolesForUser(username, ApplicationName);

            try
            {
                foreach (role_user_entity roleEntity in roles) roleNames.Add(roleEntity.role_name);
            }
            catch (DataServiceQueryException)
            {
                // If the table does not exist a DataServiceQueryException is thrown
            }

            return (string[])roleNames.ToArray(typeof(string));
        }

        public override string[] GetUsersInRole(string roleName)
        {
            ArrayList userNamesInRole = new ArrayList();

            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            IEnumerable<role_user_entity> roles = roleDataSource.GetUsersInRole(roleName, ApplicationName);

            try
            {
                foreach (role_user_entity roleEntity in roles) userNamesInRole.Add(roleEntity.user_name);
            }
            catch (DataServiceQueryException)
            {
                // If the table does not exist a DataServiceQueryException is thrown
            }

            return (string[])userNamesInRole.ToArray(typeof(string));
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            if (String.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            // Initialize the abstract base class.
            base.Initialize(name, config);

            ApplicationName = String.IsNullOrEmpty(
                config["applicationName"]) ?
                System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath :
                config["applicationName"];

            _connectionString = GetConfigValue(ConfigurationManager.AppSettings["ConnectionString"], "localhost");

            if (!int.TryParse(GetConfigValue(ConfigurationManager.AppSettings["UserIsOnlineTimeWindow"], "15"), out _userIsOnlineTimeWindow)) _userIsOnlineTimeWindow = 15;

            _adminName = GetConfigValue(ConfigurationManager.AppSettings["AdminName"], "Admin");
            _replicationFactor = Convert.ToInt32(GetConfigValue(ConfigurationManager.AppSettings["ReplicationFactor"], "1"));
        }

        public override bool IsUserInRole(string username, string rolename)
        {
            username = username.ToLower();
            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            try
            {
                return roleDataSource.IsUserInRole(username, ApplicationName, rolename);
            }
            catch (DataServiceQueryException)
            {
                return false;
            }
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] rolenames)
        {
            if (rolenames.Any(rolename => !RoleExists(rolename)))
                throw new ProviderException("Role name not found.");

            for (int i = 0; i < usernames.Count(); i++)
            {
                usernames[i] = usernames[i].ToLower();
            }

            if ((from username in usernames
                 from rolename in rolenames
                 where !IsUserInRole(username, rolename)
                 select username).Any())
                throw new ProviderException("User is not in role.");

            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            foreach (var userInRole in from username in usernames
                                       from rolename in rolenames
                                       select roleDataSource.GetUserInRole(username, ApplicationName, rolename)
                    )
                roleDataSource.Delete(userInRole.First());
        }

        public override bool RoleExists(string rolename)
        {
            CassandraTableStorageDataSource roleDataSource = new CassandraTableStorageDataSource(_connectionString, _replicationFactor);

            try
            {
                return roleDataSource.RoleExists(rolename, ApplicationName);
            }
            catch (DataServiceQueryException)
            {
                return false;
            }
        }

        #endregion | Public Methods |

        #region | Private Methods |

        /// <summary>
        /// A helper function to retrieve config values from the configuration file
        /// </summary>
        /// <param name="configValue"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private static string GetConfigValue(string configValue, string defaultValue)
        {
            string a = String.IsNullOrEmpty(configValue) ? defaultValue : configValue;
            return String.IsNullOrEmpty(configValue) ? defaultValue : configValue;
        }

        /// <summary>
        /// The provider key is a Guid made from the role name MD5 hash
        /// </summary>
        /// <param name="rolename">User name</param>
        /// <returns>Provider key</returns>
        private Guid GetProviderKeyFromRoleName(string rolename)
        {
            UnicodeEncoding unicodeEncoding = new UnicodeEncoding();
            byte[] message = unicodeEncoding.GetBytes(rolename);

            MD5 hashString = new MD5CryptoServiceProvider();

            return new Guid(hashString.ComputeHash(message));
        }

        /// <summary>
        /// The provider key is a Guid made from the username MD5 hash
        /// </summary>
        /// <param name="username">User name</param>
        /// <returns>Provider key</returns>
        private Guid GetRowKeyFromUserName(string username)
        {
            var unicodeEncoding = new UnicodeEncoding();
            var message = unicodeEncoding.GetBytes(username);

            MD5 hashString = new MD5CryptoServiceProvider();

            return new Guid(hashString.ComputeHash(message));
        }

        #endregion | Private Methods |
    }
}